/*
 * ===========================================================================
 * Copyright (C) 1999-2005 Id Software, Inc.
 * 
 * This file is part of Quake III Arena source code.
 * 
 * Quake III Arena source code is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 * 
 * Quake III Arena source code is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with 
 * Foobar; if not, write to the Free Software Foundation, Inc., 51 Franklin St, 
 * Fifth Floor, Boston, MA 02110-1301 USA
 * =========================================================================== 
 */
// 
#include "g_local.h"
#include <stddef.h>

// g_client.c -- client functions that don't happen every frame

static vec3_t playerMins = { -15, -15, -24 };
static vec3_t playerMaxs = { 15, 15, 32 };

/*
 * QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32) initial
 * potential spawning position for deathmatch games. The first time a
 * player enters the game, they will be at an 'initial' spot. Targets will 
 * be fired when someone spawns in on them. "nobots" will prevent bots
 * from using this spot. "nohumans" will prevent non-bots from using this
 * spot. 
 */
void SP_info_player_deathmatch(gentity_t * ent)
{
    int nb, nh;

    G_SpawnInt("nobots", "0", &nb);
    G_SpawnInt("nohumans", "0", &nh);
    ent->flags |= (nh * FL_NO_HUMANS) + (nb * FL_NO_BOTS);
}

/*
 * QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 32) equivelant to 
 * info_player_deathmatch 
 */
void SP_info_player_start(gentity_t * ent)
{
    ent->classname = "info_player_deathmatch";
    SP_info_player_deathmatch(ent);
}

/*
 * QUAKED info_player_intermission (1 0 1) (-16 -16 -24) (16 16 32) The
 * intermission will be viewed from this point.  Target an info_notnull
 * for the view direction. 
 */
void SP_info_player_intermission(gentity_t * ent)
{
}



/*
 * =======================================================================
 * 
 * SelectSpawnPoint
 * 
 * ======================================================================= 
 */

/*
 * =================
 * SpotWouldTelefrag
 * =================
 */
qboolean SpotWouldTelefrag(gentity_t * spot)
{
    int i, num;
    int touch[MAX_GENTITIES];
    gentity_t *hit;
    vec3_t mins, maxs;

    VectorAdd(spot->s.origin, playerMins, mins);
    VectorAdd(spot->s.origin, playerMaxs, maxs);
    num = SV_AreaEntities(mins, maxs, touch, MAX_GENTITIES);

    for (i = 0; i < num; i++)
    {
        hit = &g_entities[touch[i]];
        if (hit->client)
            return qtrue;
    }

    return qfalse;
}

/*
 * =================================
 * SelectNearestDeathmatchSpawnPoint
 *
 * Find the spot that we DON'T want to use
 * =================================
 */
#define	MAX_SPAWN_POINTS	128
gentity_t *SelectNearestDeathmatchSpawnPoint(const vec3_t from)
{
    gentity_t *spot;
    vec3_t delta;
    float dist, nearestDist;
    gentity_t *nearestSpot;

    nearestDist = 999999;
    nearestSpot = NULL;
    spot = NULL;

    while ((spot = G_Find(spot, offsetof(gentity_t, classname), "info_player_deathmatch")) != NULL)
    {
        VectorSubtract(spot->s.origin, from, delta);
        dist = VectorLength(delta);
        if (dist < nearestDist)
        {
            nearestDist = dist;
            nearestSpot = spot;
        }
    }

    return nearestSpot;
}


/*
 * ================ SelectRandomDeathmatchSpawnPoint
 * 
 * go to a random point that doesn't telefrag ================ 
 */
#define	MAX_SPAWN_POINTS	128
gentity_t *SelectRandomDeathmatchSpawnPoint(void)
{
    gentity_t *spot;
    int count;
    int selection;
    gentity_t *spots[MAX_SPAWN_POINTS];

    count = 0;
    spot = NULL;

    while ((spot =
            G_Find(spot, offsetof(gentity_t, classname), "info_player_deathmatch")) != NULL)
    {
        if (SpotWouldTelefrag(spot))
        {
            continue;
        }
        spots[count] = spot;
        count++;
    }

    if (!count)
    {                           // no spots that won't telefrag
        return G_Find(NULL, offsetof(gentity_t, classname), "info_player_deathmatch");
    }

    selection = rand() % count;
    return spots[selection];
}

/*
 * =========== SelectRandomFurthestSpawnPoint
 * 
 * Chooses a player start, deathmatch start, etc ============ 
 */
gentity_t *SelectRandomFurthestSpawnPoint(vec3_t avoidPoint, vec3_t origin,
                                          vec3_t angles)
{
    gentity_t *spot;
    vec3_t delta;
    float dist;
    float list_dist[64];
    gentity_t *list_spot[64];
    int numSpots, rnd, i, j;

    numSpots = 0;
    spot = NULL;

    while ((spot =
            G_Find(spot, offsetof(gentity_t, classname), "info_player_deathmatch")) != NULL)
    {
        if (SpotWouldTelefrag(spot))
        {
            continue;
        }
        VectorSubtract(spot->s.origin, avoidPoint, delta);
        dist = VectorLength(delta);
        for (i = 0; i < numSpots; i++)
        {
            if (dist > list_dist[i])
            {
                if (numSpots >= 64)
                    numSpots = 64 - 1;
                for (j = numSpots; j > i; j--)
                {
                    list_dist[j] = list_dist[j - 1];
                    list_spot[j] = list_spot[j - 1];
                }
                list_dist[i] = dist;
                list_spot[i] = spot;
                numSpots++;
                if (numSpots > 64)
                    numSpots = 64;
                break;
            }
        }
        if (i >= numSpots && numSpots < 64)
        {
            list_dist[numSpots] = dist;
            list_spot[numSpots] = spot;
            numSpots++;
        }
    }
    if (!numSpots)
    {
        spot = G_Find(NULL, offsetof(gentity_t, classname), "info_player_deathmatch");
        if (!spot)
            Com_Error(ERR_DROP, "Couldn't find a spawn point");
        VectorCopy(spot->s.origin, origin);
        origin[2] += 9;
        VectorCopy(spot->s.angles, angles);
        return spot;
    }
    // select a random spot from the spawn points furthest away
    rnd = (int) (random() * (numSpots / 2));

    VectorCopy(list_spot[rnd]->s.origin, origin);
    origin[2] += 9;
    VectorCopy(list_spot[rnd]->s.angles, angles);

    return list_spot[rnd];
}

/*
 * =========== SelectSpawnPoint
 * 
 * Chooses a player start, deathmatch start, etc ============ 
 */
gentity_t *SelectSpawnPoint(vec3_t avoidPoint, vec3_t origin, vec3_t angles)
{
    return SelectRandomFurthestSpawnPoint(avoidPoint, origin, angles);

    /*
     * gentity_t        *spot;
     * gentity_t        *nearestSpot;
     * 
     * nearestSpot = SelectNearestDeathmatchSpawnPoint( avoidPoint );
     * 
     * spot = SelectRandomDeathmatchSpawnPoint ( );
     * if ( spot == nearestSpot ) {
     * // roll again if it would be real close to point of death
     * spot = SelectRandomDeathmatchSpawnPoint ( );
     * if ( spot == nearestSpot ) {
     * // last try
     * spot = SelectRandomDeathmatchSpawnPoint ( );
     * }                
     * }
     * 
     * // find a single player start spot
     * if (!spot) {
     * Com_Error( "Couldn't find a spawn point" );
     * }
     * 
     * VectorCopy (spot->s.origin, origin);
     * origin[2] += 9;
     * VectorCopy (spot->s.angles, angles);
     * 
     * return spot;
     */
}

/*
 * =========== SelectInitialSpawnPoint
 * 
 * Try to find a spawn point marked 'initial', otherwise use normal spawn
 * selection. ============ 
 */
gentity_t *SelectInitialSpawnPoint(vec3_t origin, vec3_t angles)
{
    gentity_t *spot;

    spot = NULL;
    while ((spot =
            G_Find(spot, offsetof(gentity_t, classname), "info_player_deathmatch")) != NULL)
    {
        if (spot->spawnflags & 1)
        {
            break;
        }
    }

    if (!spot || SpotWouldTelefrag(spot))
    {
        return SelectSpawnPoint(vec3_origin, origin, angles);
    }

    VectorCopy(spot->s.origin, origin);
    origin[2] += 9;
    VectorCopy(spot->s.angles, angles);

    return spot;
}

/*
 * =========== SelectSpectatorSpawnPoint
 * 
 * ============ 
 */
gentity_t *SelectSpectatorSpawnPoint(vec3_t origin, vec3_t angles)
{
    FindIntermissionPoint();

    VectorCopy(level.intermission_origin, origin);
    VectorCopy(level.intermission_angle, angles);

    return NULL;
}

/*
 * =======================================================================
 * 
 * BODYQUE
 * 
 * ======================================================================= 
 */

/*
 * =============== InitBodyQue =============== 
 */
void InitBodyQue(void)
{
    int i;
    gentity_t *ent;

    level.bodyQueIndex = 0;
    for (i = 0; i < BODY_QUEUE_SIZE; i++)
    {
        ent = G_Spawn();
        ent->classname = "bodyque";
        ent->neverFree = qtrue;
        level.bodyQue[i] = ent;
    }
}

/*
 * ============= BodySink
 * 
 * After sitting around for five seconds, fall into the ground and
 * dissapear ============= 
 */
void BodySink(gentity_t * ent)
{
    if (level.time - ent->timestamp > 6500)
    {
        // the body ques are never actually freed, they are just unlinked
        SV_UnlinkEntity(ent);
        ent->physicsObject = qfalse;
        return;
    }
    ent->nextthink = level.time + 100;
    ent->s.pos.trBase[2] -= 1;
}

/*
 * ============= CopyToBodyQue
 * 
 * A player is respawning, so make an entity that looks just like the
 * existing corpse to leave behind. ============= 
 */
void CopyToBodyQue(gentity_t * ent)
{
    gentity_t *body;
    int contents;

    SV_UnlinkEntity(ent);

    // if client is in a nodrop area, don't leave the body
    contents = SV_PointContents(ent->s.origin, -1);
    if (contents & CONTENTS_NODROP)
    {
        return;
    }
    // grab a body que and cycle to the next one
    body = level.bodyQue[level.bodyQueIndex];
    level.bodyQueIndex = (level.bodyQueIndex + 1) % BODY_QUEUE_SIZE;

    SV_UnlinkEntity(body);

    body->s = ent->s;
    body->s.eFlags = EF_DEAD;   // clear EF_TALK, etc
    body->s.powerups = 0;       // clear powerups
    body->s.loopSound = 0;      // clear lava burning
    body->s.number = body - g_entities;
    body->timestamp = level.time;
    body->physicsObject = qtrue;
    body->physicsBounce = 0;    // don't bounce
    if (body->s.groundEntityNum == ENTITYNUM_NONE)
    {
        body->s.pos.trType = TR_GRAVITY;
        body->s.pos.trTime = level.time;
        VectorCopy(ent->client->ps.velocity, body->s.pos.trDelta);
    }
    else
    {
        body->s.pos.trType = TR_STATIONARY;
    }
    body->s.event = 0;

    // change the animation to the last-frame only, so the sequence
    // doesn't repeat anew for the body
    switch (body->s.legsAnim & ~ANIM_TOGGLEBIT)
    {
    case BOTH_DEATH1:
    case BOTH_DEAD1:
        body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD1;
        break;
    case BOTH_DEATH2:
    case BOTH_DEAD2:
        body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD2;
        break;
    case BOTH_DEATH3:
    case BOTH_DEAD3:
    default:
        body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD3;
        break;
    }

    body->r.svFlags = ent->r.svFlags;
    VectorCopy(ent->r.mins, body->r.mins);
    VectorCopy(ent->r.maxs, body->r.maxs);
    VectorCopy(ent->r.absmin, body->r.absmin);
    VectorCopy(ent->r.absmax, body->r.absmax);

    body->clipmask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP;
    body->r.contents = CONTENTS_CORPSE;
    body->r.ownerNum = ent->s.number;

    body->nextthink = level.time + 5000;
    body->think = BodySink;

    body->die = body_die;

    // don't take more damage if already gibbed
    if (ent->health <= GIB_HEALTH)
    {
        body->takedamage = qfalse;
    }
    else
    {
        body->takedamage = qtrue;
    }


    VectorCopy(body->s.pos.trBase, body->r.currentOrigin);
    SV_LinkEntity(body);
}

// ======================================================================


/*
 * ================== SetClientViewAngle
 * 
 * ================== 
 */
void SetClientViewAngle(gentity_t * ent, vec3_t angle)
{
    int i;

    // set the delta angle
    for (i = 0; i < 3; i++)
    {
        int cmdAngle;

        cmdAngle = ANGLE2SHORT(angle[i]);
        ent->client->ps.delta_angles[i] =
            cmdAngle - ent->client->pers.cmd.angles[i];
    }
    VectorCopy(angle, ent->s.angles);
    VectorCopy(ent->s.angles, ent->client->ps.viewangles);
}

/*
 * ================ respawn ================ 
 */
void respawn(gentity_t * ent)
{
    gentity_t *tent;

    CopyToBodyQue(ent);
    ClientSpawn(ent);

    // add a teleportation effect
    tent = G_TempEntity(ent->client->ps.origin, EV_PLAYER_TELEPORT_IN);
    tent->s.clientNum = ent->s.clientNum;
}

/*
 * ================ TeamCount
 * 
 * Returns number of players on a team ================ 
 */
unsigned int TeamCount(unsigned int ignoreClientNum, team_t team)
{
    unsigned int count = 0, i;

    for (i = 0; i < level.maxclients; i++)
    {
        if (i == ignoreClientNum)
            continue;
        if (level.clients[i].pers.connected == CON_DISCONNECTED)
            continue;
        if (level.clients[i].sess.sessionTeam == team)
            count++;
    }

    return count;
}

/*
 * ================ TeamLeader
 * 
 * Returns the client number of the team leader ================ 
 */
unsigned int TeamLeader(team_t team)
{
    unsigned int i;

    for (i = 0; i < level.maxclients; i++)
    {
        if (level.clients[i].pers.connected == CON_DISCONNECTED)
        {
            continue;
        }
        if (level.clients[i].sess.sessionTeam == team)
        {
            if (level.clients[i].sess.teamLeader)
                return i;
        }
    }

    return MAX_CLIENTS;
}


/*
 * ================ PickTeam
 * 
 * ================ 
 */
team_t PickTeam(unsigned int ignoreClientNum)
{
    int counts[TEAM_NUM_TEAMS];

    counts[TEAM_BLUE] = TeamCount(ignoreClientNum, TEAM_BLUE);
    counts[TEAM_RED] = TeamCount(ignoreClientNum, TEAM_RED);

    if (counts[TEAM_BLUE] > counts[TEAM_RED])
    {
        return TEAM_RED;
    }
    if (counts[TEAM_RED] > counts[TEAM_BLUE])
    {
        return TEAM_BLUE;
    }
    // equal team count, so join the team with the lowest score
    if (level.teamScores[TEAM_BLUE] > level.teamScores[TEAM_RED])
    {
        return TEAM_RED;
    }
    return TEAM_BLUE;
}

/*
 * =========== ForceClientSkin
 * 
 * Forces a client's skin (for teamplay) =========== 
 */
/*
 * static void ForceClientSkin( gclient_t *client, char *model, const char 
 * *skin ) { char *p;
 * 
 * if ((p = Q_strrchr(model, '/')) != 0) { *p = 0; }
 * 
 * Q_strcat(model, MAX_QPATH, "/"); Q_strcat(model, MAX_QPATH, skin); } 
 */

/*
 * =========== ClientCheckName ============ 
 */
static void ClientCleanName(const char *in, char *out, unsigned int outSize)
{
    unsigned int len, colorlessLen, spaces;
    char ch, *p;

    // save room for trailing null byte
    outSize--;

    len = 0;
    colorlessLen = 0;
    p = out;
    *p = 0;
    spaces = 0;

    while (1)
    {
        ch = *in++;
        if (!ch)
            break;

        // don't allow leading spaces
        if (!*p && ch == ' ')
            continue;

        // check colors
        if (ch == Q_COLOR_ESCAPE)
        {
            // solo trailing carat is not a color prefix
            if (!*in)
                break;

            // don't allow black in a name, period
            if (ColorIndex(*in) == 0)
            {
                in++;
                continue;
            }
            // make sure room in dest for both chars
            if (len > outSize - 2)
                break;

            *out++ = ch;
            *out++ = *in++;
            len += 2;
            continue;
        }
        // don't allow too many consecutive spaces
        if (ch == ' ')
        {
            spaces++;
            if (spaces > 3)
                continue;
        }
        else
            spaces = 0;

        if (len > outSize - 1)
            break;

        *out++ = ch;
        colorlessLen++;
        len++;
    }
    *out = 0;

    // don't allow empty names
    if (*p == 0 || colorlessLen == 0)
        Q_strncpyz(p, "UnnamedPlayer", outSize);
}


/*
 * =========== ClientUserInfoChanged
 * 
 * Called from ClientConnect when the player first connects and directly
 * by the server system when the player updates a userinfo variable.
 * 
 * The game can override any of the settings and call SV_SetUserinfo if
 * desired. ============ 
 */
void ClientUserinfoChanged(unsigned int clientNum)
{
    gentity_t *ent;
    int teamTask, teamLeader, team, health;
    char *s;
    char model[MAX_QPATH];
    char headModel[MAX_QPATH];
    char oldname[MAX_STRING_CHARS];
    gclient_t *client;
    char c1[MAX_INFO_STRING];
    char c2[MAX_INFO_STRING];
    char redTeam[MAX_INFO_STRING];
    char blueTeam[MAX_INFO_STRING];
    char userinfo[MAX_INFO_STRING];

    ent = g_entities + clientNum;
    client = ent->client;

    SV_GetUserinfo(clientNum, userinfo, sizeof(userinfo));

    // check for malformed or illegal info strings
    if (!Info_Validate(userinfo))
    {
        strcpy(userinfo, "\\name\\badinfo");
    }
    // check for local client
    s = Info_ValueForKey(userinfo, "ip");
    if (!strcmp(s, "localhost"))
    {
        client->pers.localClient = qtrue;
    }
    // check the item prediction
    s = Info_ValueForKey(userinfo, "cg_predictItems");
    if (!atoi(s))
    {
        client->pers.predictItemPickup = qfalse;
    }
    else
    {
        client->pers.predictItemPickup = qtrue;
    }

    // set name
    Q_strncpyz(oldname, client->pers.netname, sizeof(oldname));
    s = Info_ValueForKey(userinfo, "name");
    ClientCleanName(s, client->pers.netname, sizeof(client->pers.netname));

    if (client->sess.sessionTeam == TEAM_SPECTATOR)
    {
        if (client->sess.spectatorState == SPECTATOR_SCOREBOARD)
        {
            Q_strncpyz(client->pers.netname, "scoreboard",
                       sizeof(client->pers.netname));
        }
    }

    if (client->pers.connected == CON_CONNECTED)
    {
        if (strcmp(oldname, client->pers.netname))
        {
            SV_GameSendServerCommand(-1,
                                     va("print \"%s"
                                        S_COLOR_WHITE
                                        " renamed to %s\n\"",
                                        oldname, client->pers.netname));
        }
    }

    // set model
    if (g_gametype->integer >= GT_TEAM)
    {
        Q_strncpyz(model, Info_ValueForKey(userinfo, "team_model"),
                   sizeof(model));
        Q_strncpyz(headModel, Info_ValueForKey(userinfo, "team_headmodel"),
                   sizeof(headModel));
    }
    else
    {
        Q_strncpyz(model, Info_ValueForKey(userinfo, "model"), sizeof(model));
        Q_strncpyz(headModel, Info_ValueForKey(userinfo, "headmodel"),
                   sizeof(headModel));
    }

    // bots set their team a few frames later
    if (g_gametype->integer >= GT_TEAM
        && g_entities[clientNum].r.svFlags & SVF_BOT)
    {
        s = Info_ValueForKey(userinfo, "team");
        if (!Q_stricmp(s, "red") || !Q_stricmp(s, "r"))
        {
            team = TEAM_RED;
        }
        else if (!Q_stricmp(s, "blue") || !Q_stricmp(s, "b"))
        {
            team = TEAM_BLUE;
        }
        else
        {
            // pick the team with the least number of players
            team = PickTeam(clientNum);
        }
    }
    else
    {
        team = client->sess.sessionTeam;
    }

    /*
     * NOTE: all client side now // team switch( team ) { case TEAM_RED:
     * ForceClientSkin(client, model, "red"); // ForceClientSkin(client,
     * headModel, "red"); break; case TEAM_BLUE: ForceClientSkin(client,
     * model, "blue"); // ForceClientSkin(client, headModel, "blue");
     * break; } // don't ever use a default skin in teamplay, it would
     * just waste memory // however bots will always join a team but they
     * spawn in as spectator if ( g_gametype->integer >= GT_TEAM && team
     * == TEAM_SPECTATOR) { ForceClientSkin(client, model, "red"); //
     * ForceClientSkin(client, headModel, "red"); } 
     */

    // teamInfo
    s = Info_ValueForKey(userinfo, "teamoverlay");
    if (!*s || atoi(s) != 0)
    {
        client->pers.teamInfo = qtrue;
    }
    else
    {
        client->pers.teamInfo = qfalse;
    }
    /*
     * s = Info_ValueForKey( userinfo, "cg_pmove_fixed" );
     * if ( !*s || atoi( s ) == 0 ) {
     * client->pers.pmoveFixed = qfalse;
     * }
     * else {
     * client->pers.pmoveFixed = qtrue;
     * }
     */

    // team task (0 = none, 1 = offence, 2 = defence)
    teamTask = atoi(Info_ValueForKey(userinfo, "teamtask"));
    // team Leader (1 = leader, 0 is normal player)
    teamLeader = client->sess.teamLeader;

    // colors
    strcpy(c1, Info_ValueForKey(userinfo, "color1"));
    strcpy(c2, Info_ValueForKey(userinfo, "color2"));

    strcpy(redTeam, Info_ValueForKey(userinfo, "g_redteam"));
    strcpy(blueTeam, Info_ValueForKey(userinfo, "g_blueteam"));

    // send over a subset of the userinfo keys so other clients can
    // print scoreboards, display models, and play custom sounds
    if (ent->r.svFlags & SVF_BOT)
    {
        s = va
            ("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\c1\\%s\\c2\\%s\\w\\%i\\l\\%i\\skill\\%s\\tt\\%d\\tl\\%d",
             client->pers.netname, team, model, headModel, c1, c2, client->sess.wins, client->sess.losses, Info_ValueForKey(userinfo, "skill"), teamTask, teamLeader);
    }
    else
    {
        s = va
            ("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\g_redteam\\%s\\g_blueteam\\%s\\c1\\%s\\c2\\%s\\w\\%i\\l\\%i\\tt\\%d\\tl\\%d",
             client->pers.netname, client->sess.sessionTeam, model, headModel, redTeam, blueTeam, c1, c2, client->sess.wins, client->sess.losses, teamTask, teamLeader);
    }

    SV_SetConfigstring(CS_PLAYERS + clientNum, s);

    // this is not the userinfo, more like the configstring actually
    G_LogPrintf("ClientUserinfoChanged: %i %s\n", clientNum, s);
}


/*
 * =========== ClientConnect
 * 
 * Called when a player begins connecting to the server. Called again for
 * every map change or tournement restart.
 * 
 * The session information will be valid after exit.
 * 
 * Return NULL if the client should be allowed, otherwise return a string
 * with the reason for denial.
 * 
 * Otherwise, the client will be sent the current gamestate and will
 * eventually get to ClientBegin.
 * 
 * firstTime will be qtrue the very first time a client connects to the
 * server machine, but qfalse on map changes and tournement restarts.
 * ============ 
 */
char *ClientConnect(unsigned int clientNum, qboolean firstTime,
                    qboolean isBot)
{
    char *value;
    // char *areabits;
    gclient_t *client;
    char userinfo[MAX_INFO_STRING];
    gentity_t *ent;

    ent = &g_entities[clientNum];

    SV_GetUserinfo(clientNum, userinfo, sizeof(userinfo));

    // IP filtering
    // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=500
    // recommanding PB based IP / GUID banning, the builtin system is
    // pretty limited
    // check to see if they are on the banned IP list
    value = Info_ValueForKey(userinfo, "ip");
    if (G_FilterPacket(value))
    {
        return "You are banned from this server.";
    }
    // we don't check password for bots and local client
    // NOTE: local client <-> "ip" "localhost"
    // this means this client is not running in our current process
    if (!(ent->r.svFlags & SVF_BOT) && (strcmp(value, "localhost") != 0))
    {
        // check for a password
        value = Info_ValueForKey(userinfo, "password");
        if (g_password->string[0]
            && Q_stricmp(g_password->string, "none")
            && strcmp(g_password->string, value) != 0)
        {
            return "Invalid password";
        }
    }
    // they can connect
    ent->client = level.clients + clientNum;
    client = ent->client;

    // areabits = client->areabits;

    memset(client, 0, sizeof(*client));

    client->pers.connected = CON_CONNECTING;

    // read or initialize the session data
    if (firstTime || level.newSession)
    {
        G_InitSessionData(client, userinfo);
    }
    G_ReadSessionData(client);

    if (isBot)
    {
        ent->r.svFlags |= SVF_BOT;
        ent->inuse = qtrue;
        if (!G_BotConnect(clientNum, !firstTime))
        {
            return "BotConnectfailed";
        }
    }
    // get and distribute relevent paramters
    G_LogPrintf("ClientConnect: %i\n", clientNum);
    ClientUserinfoChanged(clientNum);

    // don't do the "xxx connected" messages if they were caried over from 
    // previous level
    if (firstTime)
    {
        SV_GameSendServerCommand(-1,
                                 va("print \"%s" S_COLOR_WHITE
                                    " connected\n\"", client->pers.netname));
    }

    if (g_gametype->integer >= GT_TEAM &&
        client->sess.sessionTeam != TEAM_SPECTATOR)
    {
        BroadcastTeamChange(client, -1);
    }
    // count current clients and rank for scoreboard
    CalculateRanks();

    // for statistics
    // client->areabits = areabits;
    // if ( !client->areabits )
    // client->areabits = G_Alloc(
    // (botlib_export->aas.AAS_PointReachabilityAreaIndex( NULL ) + 7) / 8 
    // );

    return NULL;
}

/*
 * =========== ClientBegin
 * 
 * called when a client has finished connecting, and is ready to be placed 
 * into the level.  This will happen every level load, and on transition
 * between teams, but doesn't happen on respawns ============ 
 */
void ClientBegin(unsigned int clientNum)
{
    gentity_t *ent;
    gclient_t *client;
    gentity_t *tent;
    int flags;

    ent = g_entities + clientNum;

    client = level.clients + clientNum;

    if (ent->r.linked)
    {
        SV_UnlinkEntity(ent);
    }
    G_InitGentity(ent);
    ent->touch = 0;
    ent->pain = 0;
    ent->client = client;

    client->pers.connected = CON_CONNECTED;
    client->pers.enterTime = level.time;
    client->pers.teamState.state = TEAM_BEGIN;

    // save eflags around this, because changing teams will
    // cause this to happen with a valid entity, and we
    // want to make sure the teleport bit is set right
    // so the viewpoint doesn't interpolate through the
    // world to the new position
    flags = client->ps.eFlags;
    memset(&client->ps, 0, sizeof(client->ps));
    client->ps.eFlags = flags;

    // locate ent at a spawn point
    ClientSpawn(ent);

    if (client->sess.sessionTeam != TEAM_SPECTATOR)
    {
        // send event
        tent = G_TempEntity(ent->client->ps.origin, EV_PLAYER_TELEPORT_IN);
        tent->s.clientNum = ent->s.clientNum;

        if (g_gametype->integer != GT_TOURNAMENT)
        {
            SV_GameSendServerCommand(-1,
                                     va("print \"%s"
                                        S_COLOR_WHITE
                                        " entered the game\n\"",
                                        client->pers.netname));
        }
    }
    G_LogPrintf("ClientBegin: %i\n", clientNum);

    // count current clients and rank for scoreboard
    CalculateRanks();
}

/*
 * =========== ClientSpawn
 * 
 * Called every time a client is placed fresh in the world: after the
 * first ClientBegin, and after each respawn Initializes all
 * non-persistant parts of playerState ============ 
 */
void ClientSpawn(gentity_t * ent)
{
    int index;
    vec3_t spawn_origin, spawn_angles;
    gclient_t *client;
    int i;
    clientPersistant_t saved;
    clientSession_t savedSess;
    int persistant[MAX_PERSISTANT];
    gentity_t *spawnPoint;
    int flags;
    int savedPing;
    // char *savedAreaBits;
    int accuracy_hits, accuracy_shots;
    int eventSequence;
    char userinfo[MAX_INFO_STRING];

    index = ent - g_entities;
    client = ent->client;

    // find a spawn point
    // do it before setting health back up, so farthest
    // ranging doesn't count this client
    if (client->sess.sessionTeam == TEAM_SPECTATOR)
    {
        spawnPoint = SelectSpectatorSpawnPoint(spawn_origin, spawn_angles);
    }
    else if (g_gametype->integer >= GT_CTF)
    {
        // all base oriented team games use the CTF spawn points
        spawnPoint = SelectCTFSpawnPoint(client->sess.sessionTeam, client->pers.teamState. state, spawn_origin, spawn_angles);
    }
    else
    {
        do
        {
            // the first spawn should be at a good looking spot
            if (!client->pers.initialSpawn && client->pers.localClient)
            {
                client->pers.initialSpawn = qtrue;
                spawnPoint = SelectInitialSpawnPoint(spawn_origin, spawn_angles);
            }
            else
            {
                // don't spawn near existing origin if possible
                spawnPoint = SelectSpawnPoint(client->ps.origin, spawn_origin, spawn_angles);
            }

            // Tim needs to prevent bots from spawning at the initial
            // point
            // on q3dm0...
            if ((spawnPoint->flags & FL_NO_BOTS) && (ent->r.svFlags & SVF_BOT))
                continue;       // try again

            // just to be symetric, we have a nohumans option...
            if ((spawnPoint->flags & FL_NO_HUMANS) && !(ent->r.svFlags & SVF_BOT))
                continue;       // try again

            break;

        }
        while (1);
    }
    client->pers.teamState.state = TEAM_ACTIVE;

    // always clear the kamikaze flag
    ent->s.eFlags &= ~EF_KAMIKAZE;

    // toggle the teleport bit so the client knows to not lerp
    // and never clear the voted flag
    flags = ent->client->ps.eFlags & (EF_TELEPORT_BIT | EF_VOTED | EF_TEAMVOTED);
    flags ^= EF_TELEPORT_BIT;

    // clear everything but the persistant data

    saved = client->pers;
    savedSess = client->sess;
    savedPing = client->ps.ping;
    // savedAreaBits = client->areabits;
    accuracy_hits = client->accuracy_hits;
    accuracy_shots = client->accuracy_shots;
    for (i = 0; i < MAX_PERSISTANT; i++)
        persistant[i] = client->ps.persistant[i];

    eventSequence = client->ps.eventSequence;

    memset(client, 0, sizeof(*client)); // bk FIXME: Com_Memset?

    client->pers = saved;
    client->sess = savedSess;
    client->ps.ping = savedPing;
    // client->areabits = savedAreaBits;
    client->accuracy_hits = accuracy_hits;
    client->accuracy_shots = accuracy_shots;
    client->lastkilled_client = -1;

    for (i = 0; i < MAX_PERSISTANT; i++)
        client->ps.persistant[i] = persistant[i];

    client->ps.eventSequence = eventSequence;
    // increment the spawncount so the client will detect the respawn
    client->ps.persistant[PERS_SPAWN_COUNT]++;
    client->ps.persistant[PERS_TEAM] = client->sess.sessionTeam;

    client->airOutTime = level.time + 12000;

    SV_GetUserinfo(index, userinfo, sizeof(userinfo));

    client->ps.eFlags = flags;

    ent->s.groundEntityNum = ENTITYNUM_NONE;
    ent->client = &level.clients[index];
    ent->takedamage = qtrue;
    ent->inuse = qtrue;
    ent->classname = "player";
    ent->r.contents = CONTENTS_BODY;
    ent->clipmask = MASK_PLAYERSOLID;
    ent->die = player_die;
    ent->waterlevel = 0;
    ent->watertype = 0;
    ent->flags = 0;

    VectorCopy(playerMins, ent->r.mins);
    VectorCopy(playerMaxs, ent->r.maxs);

    client->ps.clientNum = index;

    G_SetOrigin(ent, spawn_origin);
    VectorCopy(spawn_origin, client->ps.origin);

    // the respawned flag will be cleared after the attack and jump keys
    // come up
    client->ps.pm_flags |= PMF_RESPAWNED;

    SV_GetUsercmd(client - level.clients, &ent->client->pers.cmd);
    SetClientViewAngle(ent, spawn_angles);

    if (ent->client->sess.sessionTeam != TEAM_SPECTATOR)
    {
        G_KillBox(ent);
        SV_LinkEntity(ent);

        // force the base weapon up
        client->ps.selectedWeapon = WP_MELEE;
        client->ps.weaponState = WEAPON_READY;
    }
    // don't allow full run speed for a bit
    client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
    client->ps.pm_time = 100;

    client->respawnTime = level.time;
    client->inactivityTime = level.time + g_inactivity->integer * 1000;
    client->latched_buttons = 0;

    // set default animations
    client->ps.torsoAnim = TORSO_STAND;
    client->ps.legsAnim = LEGS_IDLE;

    if (level.intermissiontime)
        MoveClientToIntermission(ent);
    else
    {
        // fire the targets of the spawn point
        G_UseTargets(spawnPoint, ent);

        // select the highest weapon number available, after any
        // spawn given items have fired
        client->ps.selectedWeapon = 0;
    }

    // run a client frame to drop exactly to the floor,
    // initialize animations and other things
    client->ps.commandTime = level.time - 100;
    ent->client->pers.cmd.serverTime = level.time;
    ClientThink(ent - g_entities);

    // positively link the client, even if the command times are weird
    if (ent->client->sess.sessionTeam != TEAM_SPECTATOR)
    {
        BG_PlayerStateToEntityState(&client->ps, &ent->s, qtrue);
        VectorCopy(ent->client->ps.origin, ent->r.currentOrigin);
        SV_LinkEntity(ent);
    }
    // run the presend to set anything else
    ClientEndFrame(ent);

    // clear entity state values
    BG_PlayerStateToEntityState(&client->ps, &ent->s, qtrue);
    
    client->ps.weaponAmmo[WP_MELEE] = 100;
    client->ps.weaponAmmo[WP_RANGE] = 100;
    client->ps.weaponAmmo[WP_SPECIAL] = 100;
    client->ps.selectedWeapon = WP_MELEE;

    ent->health = client->ps.stats[STAT_HEALTH] = 0;
}


/*
 * =========== ClientDisconnect
 * 
 * Called when a player drops from the server. Will not be called between
 * levels.
 * 
 * This should NOT be called directly by any game logic, call
 * SV_GameDropClient(), which will call this and do server system
 * housekeeping. ============ 
 */
void ClientDisconnect(unsigned int clientNum)
{
    gentity_t *ent;
    gentity_t *tent;
    unsigned int i;

    // cleanup if we are kicking a bot that
    // hasn't spawned yet
    G_RemoveQueuedBotBegin(clientNum);

    ent = g_entities + clientNum;
    if (!ent->client)
        return;

    // stop any following clients
    for (i = 0; i < level.maxclients; i++)
    {
        if (level.clients[i].sess.sessionTeam == TEAM_SPECTATOR
            && level.clients[i].sess.spectatorState ==
            SPECTATOR_FOLLOW
            && level.clients[i].sess.spectatorClient == clientNum)
        {
            StopFollowing(&g_entities[i]);
        }
    }

    // send effect if they were completely connected
    if (ent->client->pers.connected == CON_CONNECTED
        && ent->client->sess.sessionTeam != TEAM_SPECTATOR)
    {
        tent = G_TempEntity(ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT);
        tent->s.clientNum = ent->s.clientNum;

        // They don't get to take powerups with them!
        // Especially important for stuff like CTF flags
        TossClientItems(ent);

    }

    G_LogPrintf("ClientDisconnect: %i\n", clientNum);

    // if we are playing in tourney mode and losing, give a win to the
    // other player
    if ((g_gametype->integer == GT_TOURNAMENT)
        && !level.intermissiontime
        && !level.warmupTime && level.sortedClients[1] == clientNum)
    {
        level.clients[level.sortedClients[0]].sess.wins++;
        ClientUserinfoChanged(level.sortedClients[0]);
    }

    SV_UnlinkEntity(ent);
    ent->s.modelindex = 0;
    ent->inuse = qfalse;
    ent->classname = "disconnected";
    ent->client->pers.connected = CON_DISCONNECTED;
    ent->client->ps.persistant[PERS_TEAM] = TEAM_FREE;
    ent->client->sess.sessionTeam = TEAM_FREE;

    SV_SetConfigstring(CS_PLAYERS + clientNum, "");

    CalculateRanks();

    if (ent->r.svFlags & SVF_BOT)
        BotAIShutdownClient(clientNum, qfalse);
}
